/** * Copyright 2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 the "License"; * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ package io.neba.core.resourcemodels.registration; import io.neba.api.annotations.ResourceModel; import io.neba.core.util.OsgiBeanSource; import org.apache.sling.api.resource.LoginException; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.osgi.framework.Bundle; import javax.jcr.Node; import javax.jcr.RepositoryException; import javax.jcr.nodetype.NodeType; import java.util.Collection; import java.util.HashSet; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; /** * @author Olaf Otto */ @RunWith(MockitoJUnitRunner.class) public class ModelRegistryTest { //CHECKSTYLE:OFF (All classes are the same) private static class TargetType1 {} private static class ExtendedTargetType1 extends TargetType1 {} private static class TargetType2 {} private static class TargetType3 {} private static class TargetType4 {} //CHECKSTYLE:ON @Mock private Bundle bundle; @Mock private ResourceResolver resolver; private Set<ResourceModel> resourceModelAnnotations; private long bundleId; private Collection<LookupResult> lookedUpModels; @InjectMocks private ModelRegistry testee; @Before public void setUp() throws LoginException { this.resourceModelAnnotations = new HashSet<>(); withBundleId(12345L); } @Test public void testRegistryEmptiesOnShutdown() { withBeanSources(2); assertRegistryHasModels(2); shutdownRegistry(); assertRegistryIsEmpty(); } @Test public void testUnregistrationOfModelsWhenSourceBundleIsRemoved() throws Exception { withBeanSources(2); assertRegistryHasModels(2); removeBundle(); assertRegistryIsEmpty(); } @Test public void testBeanSourceLookupByResourceType() throws Exception { withBeanSources(10); assertRegistryHasModels(10); assertRegistryFindsResourceModelsByResourceType(); } /** * The repeated lookup tests the caching behavior since a cache * is used if same lookup occurs more than once. */ @Test public void testRepeatedBeanSourceLookupByResourceType() throws Exception { withBeanSources(10); assertRegistryHasModels(10); assertRegistryFindsResourceModelsByResourceType(); assertRegistryFindsResourceModelsByResourceType(); } @Test public void testBeanSourceLookupByResourceSuperType() throws Exception { withBeanSources(10); assertRegistryHasModels(10); assertRegistryFindsResourceModelsByResourceSupertype(); } @Test public void testBeanSourceLookupForMostSpecificMapping() throws Exception { withResourceModel("some/resourcetype"); withResourceModel("some/resourcetype/supertype"); withBeanSourcesForAllResourceModels(); lookupMostSpecificBeanSources(mockResourceWithResourceSuperType("some/resourcetype", "some/resourcetype/supertype")); assertRegistryHasModels(2); assertNumberOfLookedUpBeanSourcesIs(1); } /** * The repeated lookup tests the caching behavior since a cache * is used if same lookup occurs more than once. */ @Test public void testRepeatedBeanSourceLookupForMostSpecificMapping() throws Exception { withResourceModel("some/resourcetype"); withResourceModel("some/resourcetype/supertype"); withBeanSourcesForAllResourceModels(); lookupMostSpecificBeanSources(mockResourceWithResourceSuperType("some/resourcetype", "some/resourcetype/supertype")); assertRegistryHasModels(2); assertNumberOfLookedUpBeanSourcesIs(1); lookupMostSpecificBeanSources(mockResourceWithResourceSuperType("some/resourcetype", "some/resourcetype/supertype")); assertRegistryHasModels(2); assertNumberOfLookedUpBeanSourcesIs(1); } @Test public void testMultipleMappingsToSameResourceType() throws Exception { withResourceModel("some/resourcetype"); withResourceModel("some/resourcetype"); withBeanSourcesForAllResourceModels(); lookupMostSpecificBeanSources(mockResourceWithResourceType("some/resourcetype")); assertNumberOfLookedUpBeanSourcesIs(2); } @Test public void testRemovalOfBundleWithModelforSameResourceType() throws Exception { withResourceModel("some/resourcetype"); withBundleId(1); withModelForType("some/resourcetype", TargetType1.class); withBundleId(2); withModelForType("some/resourcetype", TargetType2.class); lookupMostSpecificBeanSources(mockResourceWithResourceType("some/resourcetype")); assertNumberOfLookedUpBeanSourcesIs(2); removeBundle(); lookupMostSpecificBeanSources(mockResourceWithResourceType("some/resourcetype")); assertNumberOfLookedUpBeanSourcesIs(1); withBundleId(1); removeBundle(); lookupMostSpecificBeanSources(mockResourceWithResourceType("some/resourcetype")); assertLookedUpBeanSourcesAreNull(); } @Test public void testNoMappingsToResourceType() throws Exception { withBeanSourcesForAllResourceModels(); lookupMostSpecificBeanSources(mockResourceWithResourceType("some/resourcetype")); assertLookedUpBeanSourcesAreNull(); } @Test public void testLookupOfBeanSourceForSpecificTypeWithSingleMapping() throws Exception { withModelForType("some/resourcetype", TargetType1.class); Resource resource = mockResourceWithResourceType("some/resourcetype"); lookupBeanSourcesForType(TargetType1.class, resource); assertLookedUpBeanSourcesAreNotNull(); assertLookedUpModelTypesAre(TargetType1.class); } @Test public void testLookupOfBeanSourceForSpecificTypeWithMultipleCompatibleModels() throws Exception { withModelForType("some/resourcetype/parent", TargetType1.class); withModelForType("some/resourcetype", ExtendedTargetType1.class); Resource resource = mockResourceWithResourceSuperType("some/resourcetype", "some/resourcetype/parent"); lookupBeanSourcesForType(TargetType1.class, resource); assertLookedUpBeanSourcesAreNotNull(); assertNumberOfLookedUpBeanSourcesIs(1); } @Test public void testlookupOfBeanSourceForSpecificTypeWithoutModel() throws Exception { Resource resource = mockResourceWithResourceType("some/resourcetype"); lookupBeanSourcesForType(TargetType1.class, resource); assertLookedUpBeanSourcesAreNull(); } /** * Multiple models may apply to the same sling resource type. When queried * for a model compatible to a specific java type, only compatible models * must be provided by the registry. */ @Test public void testLookupOfBeanSourceForTypeWithMultipleIncompatibleModels() throws Exception { withModelForType("some/resourcetype", TargetType1.class); withModelForType("some/resourcetype", TargetType2.class); Resource resource = mockResourceWithResourceType("some/resourcetype"); lookupBeanSourcesForType(TargetType1.class, resource); assertLookedUpBeanSourcesAreNotNull(); assertLookedUpModelTypesAre(TargetType1.class); } /** * Different models may be provided for the same resource type. The registry must * always provide the model compatible for the desired type, regardless of whether the * mapping information was cached. */ @Test public void testRepeatedLookupOfModelWithTargetType() throws Exception { withModelForType("some/resourcetype", TargetType1.class); withModelForType("some/resourcetype", TargetType2.class); withModelForType("some/resourcetype", TargetType3.class); withModelForType("some/resourcetype", TargetType4.class); Resource resource = mockResourceWithResourceType("some/resourcetype"); lookupBeanSourcesForType(TargetType1.class, resource); assertLookedUpBeanSourcesAreNotNull(); assertLookedUpModelTypesAre(TargetType1.class); lookupBeanSourcesForType(TargetType1.class, resource); assertLookedUpBeanSourcesAreNotNull(); assertLookedUpModelTypesAre(TargetType1.class); lookupBeanSourcesForType(TargetType3.class, resource); assertLookedUpBeanSourcesAreNotNull(); assertLookedUpModelTypesAre(TargetType3.class); lookupBeanSourcesForType(TargetType2.class, resource); assertLookedUpBeanSourcesAreNotNull(); assertLookedUpModelTypesAre(TargetType2.class); lookupBeanSourcesForType(TargetType4.class, resource); assertLookedUpBeanSourcesAreNotNull(); assertLookedUpModelTypesAre(TargetType4.class); } /** * Multiple models may be compatible to the same java type and resource type. The compatible * models must be provided by the registry. */ @Test public void testLookupOfBeanSourceForTypeWithMultipleCompatibleModels() throws Exception { withModelForType("some/resourcetype", TargetType1.class); withModelForType("some/resourcetype", ExtendedTargetType1.class); Resource resource = mockResourceWithResourceType("some/resourcetype"); lookupBeanSourcesForType(TargetType1.class, resource); assertLookedUpBeanSourcesAreNotNull(); assertNumberOfLookedUpBeanSourcesIs(2); } /** * One may query the model registry for a model with a specific type and specific name for a given resource. * If a bean with the specific name exists, the registry must return it. */ @Test public void testLookupOfModelWithSpecificBeanName() throws Exception { withModelForType("some/resourcetype", TargetType1.class, "junitBeanOne"); withModelForType("some/resourcetype", ExtendedTargetType1.class, "junitBeanTwo"); Resource resource = mockResourceWithResourceType("some/resourcetype"); lookupBeanSourcesWithBeanName("junitBeanTwo", resource); assertLookedUpModelTypesAre(ExtendedTargetType1.class); } /** * The behavior tested in {@link #testLookupOfModelWithSpecificBeanName()} above * must be still consistent when the result is fetched from the cache. */ @Test public void testCachedLookupOfModelWithSpecificBeanName() throws Exception { withModelForType("some/resourcetype", TargetType1.class, "junitBeanOne"); withModelForType("some/resourcetype", ExtendedTargetType1.class, "junitBeanTwo"); Resource resource = mockResourceWithResourceType("some/resourcetype"); lookupBeanSourcesWithBeanName("junitBeanTwo", resource); // The second request also tests the result from the cache lookupBeanSourcesWithBeanName("junitBeanTwo", resource); assertLookedUpModelTypesAre(ExtendedTargetType1.class); } @Test public void testLookupOfModelWithSpecificBeanNameProvidesMostSpecificModel() throws Exception { withModelForType("some/resourcetype", TargetType1.class, "junitBean"); withModelForType("some/resource/supertype", ExtendedTargetType1.class, "junitBean"); Resource resource = mockResourceWithResourceSuperType("some/resourcetype", "some/resource/supertype"); lookupBeanSourcesWithBeanName("junitBean", resource); assertLookedUpModelTypesAre(TargetType1.class); } /** * One may query the model registry for a model with a specific type and specific name for a given resource. * If no bean with the specific name exists, the registry must return no model. */ @Test public void testLookupOfModelWithSpecificNonexistentBeanName() throws Exception { withModelForType("some/resourcetype", TargetType1.class, "junitBeanOne"); withModelForType("some/resourcetype", ExtendedTargetType1.class, "junitBeanTwo"); Resource resource = mockResourceWithResourceType("some/resourcetype"); lookupBeanSourcesWithBeanName("junitBeanThree", resource); assertLookedUpBeanSourcesAreNull(); } /** * The behavior tested in {@link #testLookupOfModelWithSpecificNonexistentBeanName()} above * must be still consistent when the result is fetched from the cache. */ @Test public void testCachedLookupOfModelWithSpecificNonexistentBeanName() throws Exception { withModelForType("some/resourcetype", TargetType1.class, "junitBeanOne"); withModelForType("some/resourcetype", ExtendedTargetType1.class, "junitBeanTwo"); Resource resource = mockResourceWithResourceType("some/resourcetype"); lookupBeanSourcesWithBeanName("junitBeanThree", resource); // The second request also tests the result from the cache lookupBeanSourcesWithBeanName("junitBeanThree", resource); assertLookedUpBeanSourcesAreNull(); } @Test public void testLookupOfAllModelsForResource() throws Exception { withModelForType("some/resourcetype/parent", TargetType1.class); withModelForType("some/resourcetype", TargetType2.class); Resource resource = mockResourceWithResourceSuperType("some/resourcetype", "some/resourcetype/parent"); lookupAllBeanSourcesFor(resource); assertLookedUpBeanSourcesAreNotNull(); assertLookedUpModelTypesAre(TargetType1.class, TargetType2.class); } @Test public void testRemovalOfInvalidReferencesToModels() throws Exception { withInvalidBeanSource("some/resource/type", TargetType1.class); assertRegistryHasModels(1); removeInvalidReferences(); assertRegistryHasModels(0); } @Test public void testValidBeanSourcesAreNotRemovedUponConsistencyCheck() throws Exception { withModelForType("some/resource/type", TargetType1.class); assertRegistryHasModels(1); removeInvalidReferences(); verifySourcesWhereTestedForValidity(); assertRegistryHasModels(1); } /** * Make sure that models for a resource's primary type do not depend on the sling:resourceType of the resource. */ @Test public void testResourceMappingForSameSlingResourceTypeAndDeviatingPrimaryType() throws Exception { withModelForType("some:JcrType", TargetType1.class); Resource resource = mockResourceWithResourceType("some/resourcetype"); withPrimaryType(resource, "some:JcrType"); lookupBeanSourcesForType(TargetType1.class, resource); assertNumberOfLookedUpBeanSourcesIs(1); withPrimaryType(resource, "nt:unstructured"); lookupBeanSourcesForType(TargetType1.class, resource); assertLookedUpBeanSourcesAreNull(); } /** * Resource may share the same sling:resourceType but have different jcr:primaryTypes. Differences in the * primary type must not affect the resources relationship to models mapping to their sling:resourceType. */ @Test public void testResourceMappingToSameModelWithDeviatingPrimaryType() throws Exception { withModelForType("my/page/type", TargetType1.class); Resource resource = mockResourceWithResourceType("my/page/type"); withPrimaryType(resource, "some:JcrType"); lookupBeanSourcesForType(TargetType1.class, resource); assertNumberOfLookedUpBeanSourcesIs(1); withPrimaryType(resource, "nt:unstructured"); lookupBeanSourcesForType(TargetType1.class, resource); assertNumberOfLookedUpBeanSourcesIs(1); } /** * Besides the primary type, sling resource type and sling resource super type of a node, * a node may also have mixin types to which a model applies. Thus, mixin types must be part of a * resource type - model type relationship and also respected when the relationship is cached. */ @Test public void testLookupDependsOnMixinTypes() throws Exception { withModelForType("mix:SomeMixing", TargetType1.class); Resource resource = mockResourceWithResourceType("my/page/type"); withPrimaryType(resource, "nt:unstructured"); withMixinTypes(resource, "mix:SomeMixing", "mix:OtherMixin"); lookupBeanSourcesForType(TargetType1.class, resource); assertNumberOfLookedUpBeanSourcesIs(1); withMixinTypes(resource, "mix:DifferentMixin", "mix:OtherMixin"); lookupBeanSourcesForType(TargetType1.class, resource); assertLookedUpBeanSourcesAreNull(); } /** * The sling resource super type of a resource may stem from either the <em>implicit</em> resource super * type, i.e. the resource type's super type, retrieved via {@link ResourceResolver#getParentResourceType(Resource)} or from * a supertype explicitly specified in the "sling:resourceSuperType" property of a resource, retrieved via * {@link Resource#getResourceSuperType()}. The latter is overriding the former, thus the registry must be sensitive to * an explicitly defined sling:resourceSuperType. */ @Test public void testCachedLookupDependsOnExplicitlyDefinedResourceSuperType() throws Exception { withModelForType("some/super/type", TargetType1.class); Resource resource = mockResourceWithResourceSuperType("my/page/type", "some/super/type"); withPrimaryType(resource, "nt:unstructured"); lookupBeanSourcesForType(TargetType1.class, resource); assertNumberOfLookedUpBeanSourcesIs(1); resource = mockResourceWithResourceType("my/page/type"); withPrimaryType(resource, "nt:unstructured"); lookupBeanSourcesForType(TargetType1.class, resource); assertLookedUpBeanSourcesAreNull(); } /** * Requires the {@link Node} to have been mocked before hand, e.g. usig {@link #withPrimaryType(Resource, String)}. */ private void withMixinTypes(Resource resource, String... mixins) throws RepositoryException { Node node = resource.adaptTo(Node.class); NodeType[] mixinTypes = new NodeType[mixins.length]; for (int i = 0; i < mixins.length; ++i) { mixinTypes[i] = mock(NodeType.class); when(mixinTypes[i].getName()).thenReturn(mixins[i]); } when(node.getMixinNodeTypes()).thenReturn(mixinTypes); } private void removeInvalidReferences() { this.testee.removeInvalidReferences(); } private void withBundleId(final long withBundleId) { this.bundleId = withBundleId; when(this.bundle.getBundleId()).thenReturn(bundleId); } private void withPrimaryType(Resource resource, String nodeTypeName) throws RepositoryException { Node node = mock(Node.class); NodeType nodeType = mock(NodeType.class); when(node.getPrimaryNodeType()).thenReturn(nodeType); when(nodeType.getName()).thenReturn(nodeTypeName); when(resource.adaptTo(Node.class)).thenReturn(node); } private void lookupAllBeanSourcesFor(Resource resource) { this.lookedUpModels = this.testee.lookupAllModels(resource); } private void lookupMostSpecificBeanSources(Resource resource) { this.lookedUpModels = this.testee.lookupMostSpecificModels(resource); } private void lookupBeanSourcesForType(Class<?> targetType, Resource resource) { this.lookedUpModels = this.testee.lookupMostSpecificModels(resource, targetType); } private void lookupBeanSourcesWithBeanName(String beanName, Resource resource) { this.lookedUpModels = this.testee.lookupMostSpecificModels(resource, beanName); } private void withResourceModel(String resourceType) { ResourceModel annotation = mock(ResourceModel.class); when(annotation.types()).thenReturn(new String[] {resourceType}); this.resourceModelAnnotations.add(annotation); } private void verifySourcesWhereTestedForValidity() { for (OsgiBeanSource<?> source : this.testee.getBeanSources()) { verify(source).isValid(); } } private void assertLookedUpModelTypesAre(Class<?>... types) { assertThat(this.lookedUpModels).extracting("source.beanType").containsOnly((Object[]) types); } private void assertNumberOfLookedUpBeanSourcesIs(int i) { assertThat(this.lookedUpModels).hasSize(i); } private void assertLookedUpBeanSourcesAreNull() { assertThat(this.lookedUpModels).isNull(); } private void assertLookedUpBeanSourcesAreNotNull() { assertThat(this.lookedUpModels).isNotNull(); } private void assertRegistryIsEmpty() { assertRegistryHasModels(0); } private void assertRegistryFindsResourceModelsByResourceType() { for (ResourceModel resourceModel : this.resourceModelAnnotations) { String resourceTypeName = resourceModel.types()[0]; Resource resource = mockResourceWithResourceType(resourceTypeName); Collection<LookupResult> models = this.testee.lookupMostSpecificModels(resource); assertThat(models).hasSize(1); } } private void assertRegistryFindsResourceModelsByResourceSupertype() { for (ResourceModel resourceModel : this.resourceModelAnnotations) { String resourceTypeName = resourceModel.types()[0]; Resource resource = mockResourceWithSupertype(resourceTypeName); Collection<LookupResult> models = this.testee.lookupMostSpecificModels(resource); assertThat(models).hasSize(1); } } private void assertRegistryHasModels(int i) { assertThat(this.testee.getBeanSources()).hasSize(i); } private Resource mockResourceWithResourceType(String resourceTypeName) { return mockResourceWithResourceSuperType(resourceTypeName, null); } private Resource mockResourceWithResourceSuperType(String resourceTypeName, String resourceSuperType) { Resource resource = mock(Resource.class); when(resource.getResourceResolver()).thenReturn(this.resolver); when(resource.getResourceType()).thenReturn(resourceTypeName); when(resource.getResourceSuperType()).thenReturn(resourceSuperType); when(this.resolver.getParentResourceType(resourceTypeName)).thenReturn(resourceSuperType); return resource; } private Resource mockResourceWithSupertype(String resourceSuperTypeTypeName) { final String resourceTypeName = "childOf/" + resourceSuperTypeTypeName; return mockResourceWithResourceSuperType(resourceTypeName, resourceSuperTypeTypeName); } private void shutdownRegistry() { this.testee.shutdown(); } private void withBeanSources(int i) { for (int k = 0; k < i; ++k) { String resourceType = "/mock/resourcetype/" + k; withResourceModel(resourceType); } withBeanSourcesForAllResourceModels(); } private void withModelForType(String resourceType, Class modelType) { withModelForType(resourceType, modelType, "defaultBeanName"); } private void withInvalidBeanSource(String resourceType, @SuppressWarnings("rawtypes") Class modelType) { withModelForType(resourceType, modelType, "defaultBeanName", false); } private void withModelForType(String resourceType, @SuppressWarnings("rawtypes") Class modelType, String modelBeanName) { withModelForType(resourceType, modelType, modelBeanName, true); } @SuppressWarnings("unchecked") private void withModelForType(String resourceType, @SuppressWarnings("rawtypes") Class modelType, String modelBeanName, boolean isValid) { OsgiBeanSource<?> source = mock(OsgiBeanSource.class); when(source.getBeanType()).thenReturn(modelType); when(source.getBundleId()).thenReturn(this.bundleId); when(source.getBeanName()).thenReturn(modelBeanName); when(source.isValid()).thenReturn(isValid); this.testee.add(new String[] {resourceType}, source); } private void withBeanSourcesForAllResourceModels() { for (ResourceModel model : this.resourceModelAnnotations) { OsgiBeanSource<?> source = mock(OsgiBeanSource.class); when(source.getBundleId()).thenReturn(this.bundleId); this.testee.add(model.types(), source); } } private void removeBundle() { this.testee.removeResourceModels(this.bundle); } }